#include "connection.h"

#include <QtNetwork>

static const int TransferTimeout = 30 * 1000;
static const int PongTimeout = 60 * 1000;
static const int PingInterval = 10 * 1000;
static const char SeparatorToken = '-';

Connection::Connection(QObject *parent)
    : QTcpSocket(parent)
{
    state = WaitingForGreeting;
    currentHeaderType = Undefined;
    numBytesForCurrentData = -1;
    transferTimerId = 0;
    isGreetingMessageSent = false;
    pingTimer.setInterval(PingInterval);

    QObject::connect(this, SIGNAL(readyRead()), this, SLOT(processReadyRead()));
    QObject::connect(this, SIGNAL(disconnected()), &pingTimer, SLOT(stop()));
    QObject::connect(&pingTimer, SIGNAL(timeout()), this, SLOT(sendPing()));
    QObject::connect(this, SIGNAL(connected()), this, SLOT(sendGreetingMessage()));
}

QString Connection::identity() const
{
    return m_identity;
}

void Connection::setIdentity(const QString &identity)
{
    m_identity = identity;
}

bool Connection::sendMessage(const QByteArray &message)
{
    return sendMessage(QByteArray("#MESSAGE") + SeparatorToken, message);
}

bool Connection::sendFile(const QByteArray &message)
{
    return sendMessage(QByteArray("#FILE") + SeparatorToken, message);
}

void Connection::timerEvent(QTimerEvent *timerEvent)
{
    if (timerEvent->timerId() == transferTimerId) {
        abort();
        killTimer(transferTimerId);
        transferTimerId = 0;
    }
}

void Connection::processReadyRead()
{
    if (state == WaitingForGreeting) {
        if (!readProtocolHeader())
            return;
        if (currentHeaderType != Greeting) {
            abort();
            return;
        }
        state = ReadingGreeting;
    }

    if (state == ReadingGreeting) {
        if (!hasEnoughData())
            return;

        m_buffer = read(numBytesForCurrentData);
        if (m_buffer.size() != numBytesForCurrentData) {
            abort();
            return;
        }

        qInfo() << "#rec greeting " << this->peerAddress() << ":" << this->peerPort();

        m_identity = m_buffer;

        currentHeaderType = Undefined;
        numBytesForCurrentData = 0;
        m_buffer.clear();

        if (!isValid()) {
            abort();
            return;
        }

        if (!isGreetingMessageSent)
            sendGreetingMessage();

        pingTimer.start();
        pongTime.start();
        state = ReadyForUse;
        emit readyForUse();
    }

     while (bytesAvailable() > 0) {
        if (currentHeaderType == Undefined) {
            if (!readProtocolHeader())
                return;
        }
        if (!hasEnoughData())
            return;

        processData();
    }
}

void Connection::sendPing()
{
    if (pongTime.elapsed() > PongTimeout) {
        abort();
        return;
    }

    write(QByteArray("#PING") + SeparatorToken + QByteArray("1").append(SeparatorToken).append("p"));
}

void Connection::sendGreetingMessage()
{
    if (sendMessage(QByteArray("#GREETING") + SeparatorToken, m_identity.toUtf8()))
    {
        isGreetingMessageSent = true;
    }
}

int Connection::readHeaderIntoBuffer(int maxSize)
{
    if (maxSize > MaxBufferSize)
        return 0;

    int numBytesBeforeRead = m_buffer.size();

    if (numBytesBeforeRead == MaxBufferSize) {
        abort();
        return 0;
    }

    while (bytesAvailable() > 0 && m_buffer.size() < maxSize) {
        m_buffer.append(read(1));
        if (m_buffer.endsWith(SeparatorToken))
            break;
    }
    return m_buffer.size() - numBytesBeforeRead;
}

int Connection::dataLengthForCurrentData()
{
    if (bytesAvailable() <= 0 || readHeaderIntoBuffer() <= 0
            || !m_buffer.endsWith(SeparatorToken))
        return 0;

    m_buffer.chop(1);
    int number = m_buffer.toInt();
    m_buffer.clear();
    return number;
}

bool Connection::readProtocolHeader()
{
    if (transferTimerId) {
        killTimer(transferTimerId);
        transferTimerId = 0;
    }

    if (readHeaderIntoBuffer() <= 0) {
        transferTimerId = startTimer(TransferTimeout);
        return false;
    }

    if (m_buffer == QByteArray("#PING").append(SeparatorToken)) {
        currentHeaderType = Ping;
    } else if (m_buffer == QByteArray("#PONG").append(SeparatorToken)) {
        currentHeaderType = Pong;
    } else if (m_buffer == QByteArray("#MESSAGE").append(SeparatorToken)) {
        currentHeaderType = Message;
    } else if (m_buffer == QByteArray("#FILE").append(SeparatorToken)) {
                currentHeaderType = FILE;
    } else if (m_buffer == QByteArray("#GREETING").append(SeparatorToken)) {
        currentHeaderType = Greeting;
    } else {
        currentHeaderType = Undefined;
        abort();
        return false;
    }
    m_buffer.clear();
    numBytesForCurrentData = dataLengthForCurrentData();
    return true;
}

bool Connection::hasEnoughData()
{
    if (transferTimerId) {
        QObject::killTimer(transferTimerId);
        transferTimerId = 0;
    }

    if (numBytesForCurrentData <= 0)
        numBytesForCurrentData = dataLengthForCurrentData();

    if (bytesAvailable() < numBytesForCurrentData || numBytesForCurrentData <= 0) {
        transferTimerId = startTimer(TransferTimeout);
        return false;
    }

    return true;
}

void Connection::processData()
{
    m_buffer = read(numBytesForCurrentData);
    if (m_buffer.size() != numBytesForCurrentData) {
        abort();
        return;
    }

    switch (currentHeaderType) {
    case Message:
        emit newMessage(m_buffer);
        break;
    case FILE:
        emit newFile(QJsonDocument::fromJson(m_buffer).toVariant().toMap());
        break;
    case Ping:
        write(QByteArray("#PONG") + SeparatorToken + QByteArray("1").append(SeparatorToken).append("p"));
        break;
    case Pong:
        pongTime.restart();
        break;
    default:
        break;
    }

    currentHeaderType = Undefined;
    numBytesForCurrentData = 0;
    m_buffer.clear();
}

bool Connection::sendMessage(const QByteArray &headerText, const QByteArray &data)
{
    if (data.isEmpty())
        return false;

    QByteArray message = headerText + QByteArray::number(data.size()) + SeparatorToken + data;
    bool ok = write(message) == message.size();
    ok = this->flush();
    this->waitForBytesWritten();

    qInfo() << "#send: " << message;

    return ok;
}
